Skip to content

feat(mcp): add no-OAuth docs-only endpoint at ?category=docs#230

Merged
andrelandgraf merged 9 commits into
mainfrom
feat/docs-only-mcp-no-oauth
May 7, 2026
Merged

feat(mcp): add no-OAuth docs-only endpoint at ?category=docs#230
andrelandgraf merged 9 commits into
mainfrom
feat/docs-only-mcp-no-oauth

Conversation

@andrelandgraf
Copy link
Copy Markdown
Collaborator

Summary

  • Adds a strict docs-only mode (?category=docs with no other category and no projectId) that bypasses OAuth entirely on /api/mcp and /api/sse, exposing only list_docs_resources and get_doc_resource.
  • Lets clients embed the Neon docs MCP tools anonymously (e.g. via mcp.neon.tech/mcp?category=docs) with no account, no OAuth flow, and no WWW-Authenticate 401 challenge.
  • Any other category combination, or the presence of projectId, keeps the existing authenticated flow unchanged.

Why it's safe

The two docs handlers (landing/mcp-src/tools/tools.ts) only call fetch(NEON_DOCS_INDEX_URL) / fetch(\${NEON_DOCS_BASE_URL}/${slug}`)and never touchneonClientorextra.account. validateDocSlug` already blocks path traversal and absolute URLs.

Implementation

  • isDocsOnlyRequest(params) helper in landing/mcp-src/utils/grant-context.ts.
  • New createDocsOnlyMcpHandler() in landing/app/api/[transport]/route.ts. It builds a mcp-handler instance without withMcpAuth, registers only docs-scoped tools, and overrides tools/list to surface only that subset. Telemetry uses a fixed anonymous-docs anonymousId.
  • handleRequest branches to the lazily-initialized docs-only handler before falling through to authHandler.
  • We deliberately don't go through getAvailableTools / grant-filter for docs-only so the always-available search / fetch tools (which need API auth) are not surfaced anonymously.

Test plan

  • pnpm run lint
  • pnpm run typecheck
  • pnpm run fmt:check
  • pnpm run knip
  • pnpm run test:unit (135/135 pass, includes 9 new cases for isDocsOnlyRequest)
  • pnpm run test:e2e:web (22/22 pass, includes 5 new cases in e2e/docs-only-mcp.spec.ts covering: initialize without auth, tools/list returns only docs tools, tools/call list_docs_resources succeeds, mixed categories still 401, and category=docs + projectId still 401)
  • Manual smoke after deploy-preview: curl -X POST 'https://preview-mcp.neon.tech/mcp?category=docs' -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-03-26\",\"capabilities\":{},\"clientInfo\":{\"name\":\"smoke\",\"version\":\"1.0.0\"}}}' → expect 200 with no WWW-Authenticate header.
  • Verify same request without ?category=docs still returns 401 with OAuth challenge.

Risk / follow-ups

  • Anonymous traffic is unbounded; the docs handlers proxy to neon.com. If abuse is a concern, add a per-IP rate limit (out of scope here).
  • /.well-known/oauth-protected-resource keeps advertising OAuth; clients only act on it after a 401, which we no longer return for docs-only requests, so this is a no-op for them.
  • If a future docs-scoped tool ever needs the Neon API client, this assumption breaks. Keep the docs scope handler set explicit.

Strict docs-only mode (?category=docs with no other category and no
projectId) bypasses withMcpAuth entirely so the documentation tools
(list_docs_resources, get_doc_resource) can be embedded anonymously
without an OAuth flow. These handlers only fetch from neon.com and
never touch the Neon API client, so anonymous access is safe.

Any other category combination, or the presence of a projectId,
keeps today's authenticated flow.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mcp-server-neon Ignored Ignored Preview May 7, 2026 4:11pm

Request Review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

🚀 Deployed to https://preview-mcp.neon.tech

This PR now owns the preview environment. OAuth flow is available for testing.

Keep anonymous docs mode independent from the authenticated tool handler map by sharing small docs fetch helpers and registering the two docs callbacks directly.
The docs-only endpoint is fully anonymous; an unauthenticated caller
that triggers a stalled neon.com fetch could otherwise hold a Vercel
concurrency slot for the full 800s function duration. Add
AbortSignal.timeout(10_000) to both fetch calls in handlers/docs.ts.

Update existing fetch-call assertions to accept the new signal arg.

Co-authored-by: Isaac
CLAUDE.md forbids third-party uptime as a merge-gating CI dependency.
The tools/call list_docs_resources case exercised a real fetch through
to neon.com via the dev server; Playwright's request.route() cannot
intercept server-side fetches, so the test is skipped with a comment
describing the dev-server-level stub needed to restore it.

Co-authored-by: Isaac
The ?category=docs branch in handleRequest is the entire OAuth-bypass
surface but only had Playwright e2e coverage. A regression that swaps
the predicate or routes docs-only requests through authHandler would
not fail merge-gating CI.

Add an integration case that POSTs /api/mcp?category=docs without an
Authorization header and asserts status<300, no WWW-Authenticate
header, and that model.getAccessToken is never consulted.

Co-authored-by: Isaac
Missed in f530ffa: mcp-server.e2e.test.ts also asserts that fetch is
called with a single argument, which now includes the AbortSignal added
to handlers/docs.ts.

Co-authored-by: Isaac
@andrelandgraf andrelandgraf merged commit af14c7c into main May 7, 2026
5 checks passed
@andrelandgraf andrelandgraf deleted the feat/docs-only-mcp-no-oauth branch May 7, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants